import frappe
from frappe import _
from frappe.utils import flt, getdate, nowdate, nowtime, rounded
import json
import traceback
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import BatchNegativeStockError
import re
from matrix_fo.module.utils.save_http_log import log_http_request

def check_attribute(obj, attr):
    """检查对象是否包含指定属性"""
    try:
        getattr(obj, attr)
        return True
    except AttributeError:
        return False


def _make_sbb(
    item_code, warehouse, batches, company,
    voucher_type, voucher_no, posting_date,
    is_outward=False, skip_fifo=False
):
    """创建通用的序列号/批号捆绑（SBB），强化负库存校验与FIFO逻辑"""
    frappe.logger().info(
        f"开始创建SBB - 物料: {item_code}, 仓库: {warehouse}, "
        f"方向: {'出库' if is_outward else '入库'}, 关联单据: {voucher_type}-{voucher_no}, "
        f"是否跳过FIFO: {skip_fifo}"
    )
    frappe.logger().debug(f"_make_sbb接收的批次列表: {json.dumps(batches, ensure_ascii=False)}")
    
    sbb = frappe.new_doc("Serial and Batch Bundle")
    sbb.company = company or frappe.db.get_default("Company")
    sbb.is_outward = 1 if is_outward else 0
    sbb.item_code = item_code
    sbb.warehouse = warehouse
    sbb.voucher_type = voucher_type
    sbb.voucher_no = voucher_no
    sbb.posting_date = posting_date
    
    if sbb.meta.has_field("type_of_transaction"):
        sbb.type_of_transaction = "Outward" if is_outward else "Inward"
    
    total_qty = 0.0
    for batch in batches:
        batch_no = batch["batch_no"].strip()
        qty = rounded(flt(batch.get("qty", 0)), 3)
        
        if qty <= 0:
            frappe.throw(f"批号 {batch_no} 的数量必须大于0（当前值：{qty}）")
        
        batch_doc = frappe.db.get_value(
            "Batch", 
            {"name": batch_no}, 
            ["item", "name", "creation", "expiry_date"],
            as_dict=True
        )
        if not batch_doc:
            frappe.throw(f"传入的批号 {batch_no} 不存在于系统中")
        if batch_doc.item != item_code:
            frappe.throw(f"批号 {batch_no} 不属于物料 {item_code}（实际属于 {batch_doc.item}）")
        
        if batch_doc.expiry_date:
            expiry_date = getdate(batch_doc.expiry_date)
            current_date = getdate(nowdate())
            if expiry_date < current_date:
                err_msg = (
                    f"批号 {batch_no} 已过期（有效期至：{expiry_date.strftime('%Y-%m-%d')}，"
                    f"当前日期：{current_date.strftime('%Y-%m-%d')}）"
                )
                frappe.logger().error(err_msg)
                frappe.local.response["http_status_code"] = 500
                frappe.throw(err_msg)
        
        if is_outward and warehouse:
            current_batch_sbbs = frappe.db.get_list(
                "Serial and Batch Entry",
                filters={"batch_no": batch_no},
                pluck="parent",
                ignore_permissions=True  
            )
            available_qty = 0.0
            if current_batch_sbbs:
                available_qty = rounded(
                    flt(frappe.db.sql("""
                        SELECT SUM(actual_qty) 
                        FROM `tabStock Ledger Entry` 
                        WHERE serial_and_batch_bundle IN %s 
                          AND warehouse = %s 
                          AND is_cancelled = 0
                    """, (tuple(current_batch_sbbs), warehouse))[0][0] or 0),
                    3
                )
            
            if available_qty < 0:
                err_msg = (
                    f"批号 {batch_no} 在仓库 {warehouse} 的库存为负数（{available_qty}），"
                    f"不允许执行出库操作，请先处理负库存"
                )
                frappe.logger().error(err_msg)
                frappe.local.response["http_status_code"] = 500
                frappe.throw(err_msg)
        
        if is_outward and warehouse and not skip_fifo:
            frappe.logger().info(f"执行FIFO校验 - 物料: {item_code}, 批号: {batch_no}, 仓库: {warehouse}")
            
            current_batch_sbbs = current_batch_sbbs or frappe.db.get_list(
                "Serial and Batch Entry",
                filters={"batch_no": batch_no},
                pluck="parent",
                ignore_permissions=True  
            )
            available_qty = available_qty if current_batch_sbbs else 0.0
            
            if available_qty < qty:
                frappe.throw(f"批号 {batch_no} 在仓库 {warehouse} 的可用量为 {available_qty}，不足 {qty}")
            
            earlier_batches = frappe.db.sql("""
                SELECT 
                    b.name AS batch_no, 
                    b.creation AS batch_creation,
                    (SELECT SUM(sle.actual_qty) 
                     FROM `tabSerial and Batch Entry` sbe
                     LEFT JOIN `tabStock Ledger Entry` sle 
                        ON sbe.parent = sle.serial_and_batch_bundle
                     WHERE sbe.batch_no = b.name 
                       AND sle.warehouse = %s 
                       AND sle.is_cancelled = 0) AS available_qty
                FROM `tabBatch` b
                WHERE 
                    b.item = %s 
                    AND b.name != %s 
                    AND b.creation < %s 
                    AND (SELECT SUM(sle.actual_qty) 
                         FROM `tabSerial and Batch Entry` sbe
                         LEFT JOIN `tabStock Ledger Entry` sle 
                            ON sbe.parent = sle.serial_and_batch_bundle
                         WHERE sbe.batch_no = b.name 
                           AND sle.warehouse = %s 
                           AND sle.is_cancelled = 0) > 0
                ORDER BY b.creation ASC 
            """, 
            (warehouse, item_code, batch_no, batch_doc.creation, warehouse), 
            as_dict=True)
            
            if earlier_batches:
                earlier_batch_info = ", ".join([
                    f"{b['batch_no']}（创建时间：{b['batch_creation'].strftime('%Y-%m-%d %H:%M')}，可用量：{rounded(flt(b['available_qty']), 3)}）"
                    for b in earlier_batches
                ])
                err_msg = (
                    f"出库违反先进先出原则：物料 {item_code} 在仓库 {warehouse} 中存在更早入库的批号，"
                    f"请优先使用以下批号：{earlier_batch_info}"
                )
                frappe.logger().error(err_msg)
                frappe.local.response["http_status_code"] = 500
                frappe.throw(err_msg)
        
        sbb.append("entries", {
            "item_code": item_code,
            "warehouse": warehouse,
            "batch_no": batch_no,
            "qty": qty
        })
        total_qty += qty
        frappe.logger().info(f"SBB添加批次 - 物料: {item_code}, 批号: {batch_no}, 业务数量: {qty}（累计：{total_qty}）")
    
    sbb.save(ignore_permissions=True)
    frappe.logger().info(f"SBB创建成功 - SBB编号: {sbb.name}, 业务总数量: {total_qty}")
    
    actual_batches = frappe.db.get_list(
        "Serial and Batch Entry",
        filters={"parent": sbb.name},
        fields=["batch_no", "qty"],
        ignore_permissions=True
    )
    actual_total_with_sign = rounded(sum(flt(b["qty"]) for b in actual_batches), 3)
    frappe.logger().debug(
        f"SBB实际存储详情 - 批次: {json.dumps(actual_batches, ensure_ascii=False)}, "
        f"实际总数量（含符号）: {actual_total_with_sign}"
    )
    
    if is_outward:
        actual_total = abs(actual_total_with_sign)
    else:
        actual_total = actual_total_with_sign
    
    if abs(actual_total - total_qty) > 1e-6:
        frappe.throw(
            f"SBB {sbb.name} 存储数量异常（业务计算值：{total_qty}，"
            f"实际存储值（含符号）：{actual_total_with_sign}）"
        )
    
    return sbb.name, total_qty


@frappe.whitelist()
def create_material_transfer_batch(items, stock_entry_type, reason_code, posting_date=None, remarks=None, **kwargs):
    """创建物料转移单据（Stock Entry），强化负库存控制与事务管理"""
    frappe.logger().info(f"=== 物料转移API请求开始 ===")
    frappe.logger().info(f"请求参数 - 移动类型: {stock_entry_type}, 原因码: {reason_code}, 过账日期: {posting_date or '默认当前日期'}")
    frappe.logger().debug(f"原始物料数据: {json.dumps(items, ensure_ascii=False)[:1000]}")
    
    try:
        if isinstance(items, str):
            frappe.logger().info(f"解析物料数据（字符串转JSON）")
            items = json.loads(items)
        items = _normalize_items(items)
        frappe.logger().info(f"物料数据规范化完成，共 {len(items)} 行物料")

        # 校验物料列表不为空（关键新增校验）
        if not items:
            err_msg = "物料列表不能为空，至少需要一行物料数据"
            frappe.logger().error(err_msg)
            frappe.throw(err_msg)

        # 跳过指定移动类型处理
        if stock_entry_type in ["ZN_CHANGESADD_TYPE", "MEDICINE_CHANGESOUT_TYPE", "MEDICINE_ADDBUCKET_TYPE"]:
            skip_msg = f"移动类型 {stock_entry_type} 已跳过处理"
            frappe.logger().info(skip_msg)
            return {
                "status": "skipped",
                "message": skip_msg,
                "stock_entry": None,
                "remarks": remarks
            }

        # 硬编码移动类型+原因映射
        hardcoded_mapping = {
            "MEDICINE_BUCKETSINK_TYPE": {
                "MEDICINE_REGULAR": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_BENGNG": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_ANALYSIS": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_PRODUCTION": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_OTHERREASONS": ("Material Consumption", "MEDICINE_DAILYADD")
            },
            "MEDICINE_ADDBUCKET_TYPE": {
                "MEDICINE_REGULAR": ("不传", "不传"),
                "MEDICINE_BENGNG": ("不传", "不传"),
                "MEDICINE_ANALYSIS": ("不传", "不传"),
                "MEDICINE_PRODUCTION": ("不传", "不传"),
                "MEDICINE_PRODUCTIONCHANGES": ("不传", "不传"),
                "MEDICINE_OTHERREASONS": ("不传", "不传")
            },
            "MEDICINE_ADDSINK_TYPE": {
                "MEDICINE_REGULAR": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_BENGNG": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_ANALYSIS": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_PRODUCTION": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Consumption", "MEDICINE_DAILYADD"),
                "MEDICINE_OTHERREASONS": ("Material Consumption", "MEDICINE_DAILYADD")
            },
            "MEDICINE_CHECKADD_TYPE": {
                "MEDICINE_REGULAR": ("Material Receipt", "ADD_MEDICINE_HD"),
                "MEDICINE_BENGNG": ("Material Receipt", "ADD_MEDICINE_HD"),
                "MEDICINE_ANALYSIS": ("Material Receipt", "ADD_MEDICINE_HD"),
                "MEDICINE_PRODUCTION": ("Material Receipt", "ADD_MEDICINE_HD"),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Receipt", "ADD_MEDICINE_HD"),
                "MEDICINE_OTHERREASONS": ("Material Receipt", "ADD_MEDICINE_HD")
            },
            "MEDICINE_CHECKOUT_TYPE": {
                "MEDICINE_REGULAR": ("Material Issue", "OUT_MEDICINE_HD"),
                "MEDICINE_BENGNG": ("Material Issue", "OUT_MEDICINE_HD"),
                "MEDICINE_ANALYSIS": ("Material Issue", "OUT_MEDICINE_HD"),
                "MEDICINE_PRODUCTION": ("Material Issue", "OUT_MEDICINE_HD"),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Issue", "OUT_MEDICINE_HD"),
                "MEDICINE_OTHERREASONS": ("Material Issue", "OUT_MEDICINE_HD")
            },
            "ZN_CHANGESOUT_TYPE": {
                "ZN_REGULAR": ("Material Consumption", "ZN_OUT"),
                "ZN_NG": ("Material Consumption", "ZN_OUT"),
                "ZN_ANALYSIS": ("Material Consumption", "ZN_OUT"),
                "ZN_PRODUCTION": ("Material Consumption", "ZN_OUT"),
                "ZN_OTHERREASONS": ("Material Consumption", "ZN_OUT"),
                "MEDICINE_REGULAR": ("Material Consumption", "ZN_CHANGESOUT_TYPE"),
                "MEDICINE_BENGNG": ("Material Consumption", "ZN_CHANGESOUT_TYPE"),
                "MEDICINE_ANALYSIS": ("Material Consumption", "ZN_CHANGESOUT_TYPE"),
                "MEDICINE_PRODUCTION": ("Material Consumption", "ZN_CHANGESOUT_TYPE"),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Consumption", "ZN_CHANGESOUT_TYPE"),
                "MEDICINE_OTHERREASONS": ("Material Consumption", "ZN_CHANGESOUT_TYPE")
            },
            "INVENTORY_MOVE": {
                "RAW_MATERIAL_OUT": ("Material Transfer", ""),
                "MEDICINE_REGULAR": ("Material Transfer", ""),
                "MEDICINE_BENGNG": ("Material Transfer", ""),
                "MEDICINE_ANALYSIS": ("Material Transfer", ""),
                "MEDICINE_PRODUCTION": ("Material Transfer", ""),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Transfer", ""),
                "MEDICINE_OTHERREASONS": ("Material Transfer", "")
            },
            "MEDICINE_CHANGESADD_TYPE": {
                "MEDICINE_REGULAR": ("Material Consumption", "ADD_MEDICINE_HD"),
                "MEDICINE_BENGNG": ("Material Consumption", "ADD_MEDICINE_HD"),
                "MEDICINE_ANALYSIS": ("Material Consumption", "ADD_MEDICINE_HD"),
                "MEDICINE_PRODUCTION": ("Material Consumption", "ADD_MEDICINE_HD"),
                "MEDICINE_PRODUCTIONCHANGES": ("Material Consumption", "ADD_MEDICINE_HD"),
                "MEDICINE_OTHERREASONS": ("Material Consumption", "ADD_MEDICINE_HD")
            },
        }

        # 校验移动类型和原因合法性
        if stock_entry_type not in hardcoded_mapping:
            err_msg = f"不支持的移动类型：{stock_entry_type}"
            frappe.logger().error(err_msg)
            frappe.throw(err_msg)
        if reason_code not in hardcoded_mapping[stock_entry_type]:
            err_msg = f"移动类型 {stock_entry_type} 不支持原因：{reason_code}"
            frappe.logger().error(err_msg)
            frappe.throw(err_msg)
        
        base_mapped_stock_type, base_mapped_reason = hardcoded_mapping[stock_entry_type][reason_code]
        frappe.logger().info(f"映射结果 - 单据类型: {base_mapped_stock_type}, 原因码: {base_mapped_reason}")
        
        if base_mapped_stock_type == "不传" or base_mapped_reason == "不传":
            err_msg = f"移动类型 {stock_entry_type} + 原因 {reason_code} 配置为「不传」，禁止传输"
            frappe.logger().error(err_msg)
            frappe.throw(err_msg)

        posting_date = posting_date or nowdate()

        # 定义单据方向类型
        inward_types = {"Material Receipt"}
        outward_types = {"Material Issue", "Material Consumption"}
        transfer_types = {"Material Transfer"}

        # 字段选择规则
        field_rules = {
            "itemcode": {
                "send": ["MEDICINE_DAILYADD", "MEDICINE_IN_LONG", "MEDICINE_IN_SHORT", "OUT_MEDICINE_HD", "ZN_CHANGESOUT_TYPE", "RAW_MATERIAL_OUT", "ZN_OUT"],
                "receive": ["ADD_MEDICINE_HD", "MEDICINE_OUT"]
            },
            "s_warehouse": {
                "send": ["MEDICINE_DAILYADD", "MEDICINE_IN_LONG", "MEDICINE_IN_SHORT", "OUT_MEDICINE_HD", "ZN_CHANGESOUT_TYPE", "RAW_MATERIAL_OUT", "ZN_OUT"],
                "receive": []
            },
            "t_warehouse": {
                "send": [],
                "receive": ["ADD_MEDICINE_HD", "MEDICINE_OUT", "RAW_MATERIAL_OUT","MEDICINE_IN_LONG", "MEDICINE_IN_SHORT"]
            },
            "qty": {
                "send": ["MEDICINE_DAILYADD", "MEDICINE_IN_LONG", "MEDICINE_IN_SHORT", "OUT_MEDICINE_HD", "ZN_CHANGESOUT_TYPE", "RAW_MATERIAL_OUT", "ZN_OUT"],
                "receive": ["ADD_MEDICINE_HD", "MEDICINE_OUT"]
            },
            "cost_center": {
                "send": ["ZN_CHANGESOUT_TYPE", "ZN_OUT"],
                "receive": ["MEDICINE_DAILYADD", "MEDICINE_IN_LONG", "MEDICINE_IN_SHORT", "ADD_MEDICINE_HD", "MEDICINE_OUT"]
            }
        }

        # 费用科目映射
        expense_account_mapping = {
            "MEDICINE_IN_LONG": "1801 - 长期待摊费用 - M"
        }

        # 1. 创建Stock Entry文档（暂不保存，先构建数据）
        doc = frappe.new_doc("Stock Entry")
        frappe.logger().info(f"开始创建Stock Entry - 类型: {base_mapped_stock_type}")
        
        # 设置主表字段
        if check_attribute(doc, "stock_entry_type"):
            doc.stock_entry_type = base_mapped_stock_type
        if check_attribute(doc, "purpose"):
            doc.purpose = base_mapped_stock_type
        if check_attribute(doc, "posting_date"):
            doc.posting_date = posting_date
        if check_attribute(doc, "posting_time"):
            doc.posting_time = kwargs.get("posting_time") or nowtime()
        
        if stock_entry_type != "MEDICINE_CHANGESADD_TYPE" and check_attribute(doc, "reason_code") and base_mapped_reason:
            doc.reason_code = base_mapped_reason
        
        if remarks and check_attribute(doc, "custom_remarks"):
            doc.custom_remarks = remarks
            frappe.logger().info(f"添加备注: {remarks}")
        elif remarks:
            frappe.log_warning("Stock Entry文档没有custom_remarks字段，无法存储备注信息", "备注存储警告")

        # 2. 处理物料明细行（先添加到items子表）
        sbb_list = []
        item_cost_centers = []
        for idx, row in enumerate(items, 1):
            frappe.logger().info(f"处理第 {idx} 行物料 - 原始数据: {json.dumps(row, ensure_ascii=False)[:500]}")
            current_mapped_stock_type = base_mapped_stock_type
            current_mapped_reason = base_mapped_reason

            if stock_entry_type == "MEDICINE_CHANGESADD_TYPE":
                cost_center_field = "receive_carrier_no"
                current_cost_center = row.get(cost_center_field)
                if not current_cost_center:
                    err_msg = f"物料行缺少成本中心字段（{cost_center_field}），无法确定移动原因"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)
                
                if not frappe.db.exists("Cost Center", current_cost_center):
                    err_msg = f"成本中心 {current_cost_center} 不存在，请检查字段 {cost_center_field} 的值"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)
                
                current_mapped_reason = "MEDICINE_IN_SHORT" if current_cost_center.strip().upper() in ["A02", "A03"] else "MEDICINE_IN_LONG"
                if check_attribute(doc, "reason_code") and not doc.reason_code:
                    doc.reason_code = current_mapped_reason
                frappe.logger().info(f"MEDICINE_CHANGESADD_TYPE 映射 - 成本中心: {current_cost_center}, 原因码: {current_mapped_reason}")

            # 字段选择逻辑
            if stock_entry_type == "ZN_CHANGESOUT_TYPE":
                field_selection = {
                    "itemcode": "send_itemcode",
                    "s_warehouse": "send_storage_location",
                    "t_warehouse": None,
                    "qty": "send_qty",
                    "cost_center": "send_carrier_no"
                }
            elif stock_entry_type == "INVENTORY_MOVE":
                field_selection = {
                    "itemcode": "send_itemcode",
                    "s_warehouse": "send_storage_location",
                    "t_warehouse": "receive_storage_location",
                    "qty": "send_qty",
                    "cost_center": "send_carrier_no"
                }
            else:
                field_selection = {}
                if current_mapped_reason in field_rules["itemcode"]["send"]:
                    field_selection["itemcode"] = "send_itemcode"
                elif current_mapped_reason in field_rules["itemcode"]["receive"]:
                    field_selection["itemcode"] = "receive_itemcode"
                else:
                    err_msg = f"不支持的物料编码场景：{stock_entry_type}, {current_mapped_reason}"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)

                if current_mapped_reason in field_rules["s_warehouse"]["send"]:
                    field_selection["s_warehouse"] = "send_storage_location"
                else:
                    field_selection["s_warehouse"] = None

                if current_mapped_reason in field_rules["t_warehouse"]["receive"]:
                    field_selection["t_warehouse"] = "receive_storage_location"
                else:
                    field_selection["t_warehouse"] = None

                if current_mapped_reason in field_rules["qty"]["send"]:
                    field_selection["qty"] = "send_qty"
                elif current_mapped_reason in field_rules["qty"]["receive"]:
                    field_selection["qty"] = "receive_qty"
                else:
                    err_msg = f"不支持的数量场景：{stock_entry_type}, {current_mapped_reason}"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)

                if "cost_center" in field_rules and stock_entry_type != "MEDICINE_CHANGESADD_TYPE":
                    if current_mapped_reason in field_rules["cost_center"]["send"]:
                        field_selection["cost_center"] = "send_carrier_no"
                    elif current_mapped_reason in field_rules["cost_center"]["receive"]:
                        field_selection["cost_center"] = "receive_carrier_no"
                    else:
                        field_selection["cost_center"] = "send_carrier_no"
                elif stock_entry_type != "MEDICINE_CHANGESADD_TYPE":
                    field_selection["cost_center"] = "send_carrier_no"

            frappe.logger().info(f"第 {idx} 行字段映射: {json.dumps(field_selection, ensure_ascii=False)}")

            # 获取当前行字段值
            item_code = row.get(field_selection["itemcode"], "").strip()
            if not item_code:
                err_msg = f"物料行缺少必填字段：{field_selection['itemcode']}（物料号）"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)

            s_warehouse = row.get(field_selection["s_warehouse"], "").strip() if field_selection["s_warehouse"] else None
            t_warehouse = row.get(field_selection["t_warehouse"], "").strip() if field_selection["t_warehouse"] else None
            qty = rounded(flt(row.get(field_selection["qty"], 0)), 3)
            
            # 成本中心赋值
            if stock_entry_type == "INVENTORY_MOVE" and reason_code == "RAW_MATERIAL_OUT":
                cost_center = None
            elif stock_entry_type == "ZN_CHANGESOUT_TYPE":
                cost_center = row.get(field_selection["cost_center"], "").strip()
            elif stock_entry_type in ["MEDICINE_CHECKADD_TYPE", "MEDICINE_CHECKOUT_TYPE"]:
                cost_center = "A100 - M"
            elif stock_entry_type == "MEDICINE_CHANGESADD_TYPE":
                cost_center = current_cost_center
            else:
                cost_center = row.get(field_selection["cost_center"], "").strip()
            
            uom = row.get("uom", "").strip()

            # 基础校验
            if field_selection["s_warehouse"] and not s_warehouse:
                err_msg = f"物料 {item_code} 缺少必填字段：{field_selection['s_warehouse']}（源仓库）"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
            if stock_entry_type == "ZN_CHANGESOUT_TYPE" and t_warehouse:
                err_msg = f"移动类型 {stock_entry_type} 不允许填写目标仓库，物料 {item_code} 需移除t_warehouse"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
            if qty <= 0:
                err_msg = f"物料 {item_code} 的数量 {qty} 无效（必须大于0）"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
            if not (stock_entry_type == "INVENTORY_MOVE" and reason_code == "RAW_MATERIAL_OUT") and not cost_center:
                missing_field = field_selection["cost_center"] if stock_entry_type != "MEDICINE_CHANGESADD_TYPE" else "receive_carrier_no"
                err_msg = f"物料 {item_code} 缺少必填字段：{missing_field}（成本中心）"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
            if not uom:
                err_msg = f"物料 {item_code} 缺少必填字段：uom（单位）"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)

            # 检查物料是否为库存物料及批次管理状态
            item_detail = frappe.get_doc("Item", item_code)
            if not item_detail.is_stock_item:
                err_msg = f"物料 {item_code} 不是库存物料，无法用于库存交易"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
            
            # 提取传入的批号信息
            input_batch_no = row.get("batch_no", "").strip()
            input_batches = row.get("batches", [])
            frappe.logger().debug(f"物料 {item_code} 原始批号信息 - batch_no: {input_batch_no}, batches: {json.dumps(input_batches, ensure_ascii=False)}")

            # 规范化batches结构
            batches = []
            if input_batches:
                if not isinstance(input_batches, list):
                    err_msg = f"物料 {item_code} 的batches必须是数组，当前类型：{type(input_batches).__name__}"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)
                for b in input_batches:
                    if not isinstance(b, dict) or "batch_no" not in b or "qty" not in b:
                        err_msg = f"物料 {item_code} 的batches格式错误，需包含batch_no和qty"
                        frappe.logger().error(err_msg)
                        frappe.throw(err_msg)
                    batch_no = b["batch_no"].strip()
                    batch_qty = rounded(flt(b["qty"]), 3)
                    if not batch_no:
                        err_msg = f"物料 {item_code} 的batches包含空批号"
                        frappe.logger().error(err_msg)
                        frappe.throw(err_msg)
                    if batch_qty <= 0:
                        err_msg = f"物料 {item_code} 的批号 {batch_no} 数量必须大于0"
                        frappe.logger().error(err_msg)
                        frappe.throw(err_msg)
                    batches.append({"batch_no": batch_no, "qty": batch_qty})
                total_batch_qty = sum(flt(b["qty"]) for b in batches)
                if abs(total_batch_qty - qty) > 1e-6:
                    err_msg = f"物料 {item_code} 多批次总数量 {total_batch_qty} 与行数量 {qty} 不一致"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)
                frappe.logger().info(f"物料 {item_code} 多批号处理 - 共 {len(batches)} 个批号，总数量: {total_batch_qty}")
            elif input_batch_no:
                batches = [{"batch_no": input_batch_no, "qty": qty}]
                frappe.logger().info(f"物料 {item_code} 单批号处理 - 批号: {input_batch_no}, 数量: {qty}")
            else:
                if item_detail.has_batch_no:
                    err_msg = f"物料 {item_code} 启用了批号管理，请传入batch_no或batches"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)
                frappe.logger().info(f"物料 {item_code} 非批号管理，无需批号信息")

            # 强化校验：batches中的批号必须与输入一致
            if item_detail.has_batch_no:
                input_batch_set = {input_batch_no} if input_batch_no else {b["batch_no"] for b in input_batches}
                actual_batch_set = {b["batch_no"] for b in batches}
                if input_batch_set != actual_batch_set:
                    err_msg = (
                        f"物料 {item_code} 批号信息异常："
                        f"传入批号 {input_batch_set}，实际处理批号 {actual_batch_set}"
                    )
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)

            # 整理custom_batch_no显示内容
            if input_batches:
                custom_batch_str = ", ".join([f"{b['batch_no']}:{b['qty']}" for b in input_batches])
            elif input_batch_no:
                custom_batch_str = input_batch_no
            else:
                custom_batch_str = ""

            # 批次管理物料必须通过SBB处理
            sbb_name = None
            if item_detail.has_batch_no:
                # 确定SBB方向和关联仓库
                if current_mapped_stock_type in transfer_types:
                    sbb_warehouse = s_warehouse
                    is_outward = True
                elif current_mapped_stock_type in outward_types:
                    sbb_warehouse = s_warehouse
                    is_outward = True
                elif current_mapped_stock_type in inward_types:
                    sbb_warehouse = t_warehouse
                    is_outward = False
                else:
                    err_msg = f"未知单据类型 {current_mapped_stock_type}，无法确定SBB方向"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)

                # 计算是否跳过FIFO
                skip_fifo = not (
                    current_mapped_stock_type in transfer_types
                    and sbb_warehouse not in ["HX01-005", "HX02-003"]
                )

                if skip_fifo:
                    if current_mapped_stock_type not in transfer_types:
                        skip_reason = f"移动类型为{current_mapped_stock_type}（非直接调拨）"
                    else:
                        skip_reason = f"发出仓库为{sbb_warehouse}（属于HX01-005/HX02-003）"
                    frappe.logger().info(f"物料 {item_code} 跳过FIFO校验 - 原因：{skip_reason}")
                else:
                    frappe.logger().info(f"物料 {item_code} 执行FIFO校验 - 原因：移动类型为直接调拨且发出仓库非HX01-005/HX02-003")

                # 创建SBB（此时还未保存Stock Entry，暂时不传入voucher_no，后续调整）
                # 注意：这里先创建SBB，后续Stock Entry保存后需要更新SBB的voucher_no
                sbb_name, total_batch_qty = _make_sbb(
                    item_code=item_code,
                    warehouse=sbb_warehouse,
                    batches=batches,
                    company=doc.company or frappe.db.get_default("Company"),
                    voucher_type="Stock Entry",
                    voucher_no=None,  # 临时为空，后续更新
                    posting_date=doc.posting_date,
                    is_outward=is_outward,
                    skip_fifo=True
                )

                if abs(total_batch_qty - qty) > 1e-6:
                    err_msg = f"物料 {item_code} 的批次总数量 {total_batch_qty} 与行数量 {qty} 不一致"
                    frappe.logger().error(err_msg)
                    frappe.throw(err_msg)

                sbb_list.append({
                    "item_code": item_code,
                    "sbb_no": sbb_name,
                    "batch_info": batches,
                    "sbb_direction": "Outward" if is_outward else "Inward",
                    "fifo_executed": not skip_fifo
                })
                frappe.logger().info(f"物料 {item_code} 关联SBB - SBB编号: {sbb_name}")
            else:
                frappe.logger().info(f"物料 {item_code} 非批次管理，不创建SBB")

            # 构建物料明细行
            item_row = {
                "item_code": item_code,
                "qty": qty,
                "uom": uom,
                "s_warehouse": s_warehouse,
                "t_warehouse": t_warehouse if current_mapped_stock_type not in outward_types else None,
                "cost_center": cost_center,
                "reason_code": current_mapped_reason,
                "expense_account": expense_account_mapping.get(current_mapped_reason, ""),
                "custom_batch_no": custom_batch_str
            }
            if current_mapped_stock_type == "Material Consumption":
                item_row["t_warehouse"] = None
                frappe.logger().info(f"物料 {item_code} 为耗用类型，清空目标仓库")

            if item_detail.has_batch_no:
                item_row["serial_and_batch_bundle"] = sbb_name

            item_cost_centers.append({
                "item_code": item_code,
                "actual_cost_center": cost_center,
                "mapped_reason": current_mapped_reason
            })

            # 添加明细行到单据（关键：先添加到items子表）
            doc.append("items", item_row)
            frappe.logger().info(f"第 {idx} 行物料处理完成 - 物料: {item_code}, 数量: {qty}")

        # 3. 所有物料明细行添加完成后，再保存Stock Entry（关键修复：调整顺序）
        doc.insert(ignore_permissions=True)
        frappe.logger().info(f"Stock Entry 保存成功 - 单据编号: {doc.name}")

        # 4. 更新SBB的voucher_no（关联已保存的Stock Entry编号）
        for sbb_info in sbb_list:
            sbb_doc = frappe.get_doc("Serial and Batch Bundle", sbb_info["sbb_no"])
            sbb_doc.voucher_no = doc.name  # 关联Stock Entry编号
            sbb_doc.save(ignore_permissions=True)
            frappe.logger().info(f"SBB {sbb_info['sbb_no']} 关联Stock Entry {doc.name} 成功")

        # 5. 提交Stock Entry
        doc.submit()
        frappe.logger().info(f"Stock Entry 提交成功 - 单据编号: {doc.name}")

        return {
            "status": "success",
            "message": f"Stock Entry {doc.name} 创建成功",
            "stock_entry": doc.name,
            "base_mapped_stock_type": base_mapped_stock_type,
            "base_mapped_reason_code": base_mapped_reason,
            "sbb_list": sbb_list,
            "item_cost_centers": item_cost_centers,
            "remarks": remarks
        }

    except Exception as e:
        frappe.db.rollback()
        
        error_details = traceback.format_exc()
        frappe.logger().error(f"物料转移API执行失败: {str(e)}\n{error_details}")
        frappe.log_error(error_details, "物料转移批量处理错误")
        frappe.local.response["http_status_code"] = 500
        return {
            "status": "error",
            "message": str(e),
            "details": error_details
        }
    finally:
        frappe.db.commit()
        frappe.logger().info(f"=== 物料转移API请求结束 ===\n")


def _normalize_items(items):
    """规范化输入的物料数据格式，确保批号和数量字段正确解析"""
    frappe.logger().info(f"开始规范化物料数据 - 原始类型: {type(items).__name__}")
    
    if isinstance(items, str):
        try:
            items = json.loads(items)
            frappe.logger().info("物料数据字符串成功转换为JSON")
        except json.JSONDecodeError as e:
            err_msg = f"物料数据JSON解析失败：{str(e)}"
            frappe.logger().error(err_msg)
            frappe.throw(err_msg)
    
    if isinstance(items, dict) and items and all(isinstance(v, (dict, str)) for v in items.values()):
        items = list(items.values())
        frappe.logger().info("物料数据字典转换为列表")
    
    if not isinstance(items, list):
        err_msg = f"`items` 必须是数组（list），当前类型：{type(items).__name__}"
        frappe.logger().error(err_msg)
        frappe.throw(err_msg)
    
    normalized = []
    for i, row in enumerate(items, 1):
        if isinstance(row, str):
            try:
                row = json.loads(row)
                frappe.logger().info(f"第 {i} 行物料字符串转换为JSON")
            except json.JSONDecodeError as e:
                err_msg = f"第 {i} 行物料数据JSON解析失败：{str(e)}"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
        
        if not isinstance(row, dict):
            err_msg = f"第 {i} 行数据不是对象，类型：{type(row).__name__}"
            frappe.logger().error(err_msg)
            frappe.throw(err_msg)
        
        if "batches" in row and isinstance(row["batches"], str):
            try:
                row["batches"] = json.loads(row["batches"])
                frappe.logger().info(f"第 {i} 行批次字符串转换为JSON数组")
            except json.JSONDecodeError as e:
                err_msg = f"第 {i} 行批次数据JSON解析失败：{str(e)}"
                frappe.logger().error(err_msg)
                frappe.throw(err_msg)
        
        for field in ["send_qty", "receive_qty", "qty"]:
            if field in row:
                row[field] = rounded(flt(row[field]), 3)
        
        string_fields = [
            "send_itemcode", "receive_itemcode", "item_code",
            "send_storage_location", "receive_storage_location", 
            "send_carrier_no", "receive_carrier_no",
            "cost_center", "uom", "batch_no"
        ]
        for field in string_fields:
            if field in row:
                row[field] = str(row[field]).strip() if row[field] is not None else ""
        
        normalized.append(row)
    
    frappe.logger().info(f"物料数据规范化完成 - 共 {len(normalized)} 行")
    return normalized


@frappe.whitelist()
@log_http_request
def create_mes_material_movement():
    """
    MES -> ERPNext 物料移动接口
    根据 MES Movement Mapping 表动态映射字段
    """
    try:
        
        frappe.db.begin()  # 开启事务
        data = frappe.parse_json(frappe.local.form_dict)
        # data = frappe.local.form_dict

        mes_move_type = data.get("stock_entry_type")
        mes_reason_code = data.get("reason_code")
        items = data.get("items") or []
        # frappe.logger().error(f"{mes_reason_code} and {mes_move_type}")
        # #当type和code是这种特殊情况时
        if mes_move_type == 'ZN_CHANGESOUT_TYPE' and mes_reason_code == 'ZN_NG':
            # frappe.logger().error(f"special move_type and reason_code!")
            if len(items)<2:
                return{
                    "status":"error",
                    "message":"when move_type and reason_code is ZN_CHANGESOUT_TYPE and ZN_NG.items must is 2 rows",
                }
            temp=items[0]
            item_code = temp["send_itemcode"]
            batch_no =temp.get("batch_no")
            has_batch_no_1=frappe.get_value(
                "Item",
                item_code,
                "has_batch_no"
            )
            #若开启了批次号管理，批次号做校验
            if has_batch_no_1 == 1:
                if not batch_no:
                    frappe.log_error(frappe.get_traceback(), "batch_no")
                    frappe.local.response["http_status_code"] = 500
                    return{
                        "status":"error",
                        "message":f"in speacail move type error,that item_code{item_code} has batch_no control,but you didn't give batch_no"
                    }

            # ref_value = frappe.db.get_value("Cost Center", temp.get("send_carrier_no",None), "cost_center_name")
            # if not ref_value:
            #     frappe.throw(f"成本中心 {cost_center} 未维护字段 {ref_field}")
            #第一张单
            stock_entry_1 = frappe.new_doc("Stock Entry")
            stock_entry_1.stock_entry_type = "Material consumption"
            stock_entry_1.purpose = "Material Issue"
            l_temp={
                "s_warehouse":temp["send_storage_location"],  #发料
                "qty":temp["send_qty"],
                "item_code":temp["send_itemcode"],
                "uom":temp["uom"],
                "cost_center":temp["send_carrier_no"],
                "batch_no":temp["batch_no"],
                "use_serial_batch_fields":has_batch_no_1,
            }
            stock_entry_1.append("items",l_temp)

            #第二张单
            stock_entry_2 = frappe.new_doc("Stock Entry")
            stock_entry_2.stock_entry_type = "Repack"
            stock_entry_2.purpose = "Repack"
            temp_2=items[1]
            item_code = temp_2.get("send_itemcode")
            batch_no =temp.get("batch_no")
            has_batch_no_2=frappe.get_value(
                "Item",
                item_code,
                "has_batch_no"
            )
            #若开启了批次号管理，批次号做校验
            if has_batch_no_2 == 1:
                if not batch_no:
                    frappe.throw(f"缺少批次号 (batch_no)")
            l_temp_2={
                "item_code":temp_2["receive_itemcode"],
                "t_warehouse":temp_2["receive_storage_location"],  #收料
                "qty":temp_2["receive_qty"],
                "uom":temp_2["uom"],
                "cost_center":temp_2["send_carrier_no"],
                "batch_no":temp_2["batch_no"],
                "use_serial_batch_fields":has_batch_no_2,
            }
            stock_entry_2.append("items",l_temp)
            stock_entry_2.append("items",l_temp_2)
            try:
                stock_entry_1.insert()
                stock_entry_2.insert()
                stock_entry_1.submit()
                stock_entry_2.submit()
                frappe.db.commit()
            except Exception as e:
                frappe.db.rollback()
                frappe.log_error(frappe.get_traceback(), "MES Material Movement Error")
                frappe.local.response["http_status_code"] = 500
                return{
                    "status":"error",
                    "message":f"in speacail move type error,msg:{e}"
                }
            return {
                "status":"success",
                "stock_entry_no":f"{stock_entry_1.name},{stock_entry_2.name}"
            }

        ###
                
        # 1. 查找主数据映射记录
        mapping = frappe.db.get_value(
            "MES Movement Mapping",
            {
                "mes_move_type": mes_move_type,
                "mes_reason_code": mes_reason_code,
            },
            [
                "en_move_type",
                "en_reason_code",
                "item_field",
                "source_wh_field",
                "target_wh_field",
                "qty_field",
                "cost_center_field",
                "move_expense_account",
            ],
            as_dict=True
        )
        
        if not mapping:
            frappe.throw(f"未找到对应的主数据配置: {mes_move_type}/{mes_reason_code}")

        # 2. 创建 Stock Entry
        stock_entry = frappe.new_doc("Stock Entry")
        stock_entry.stock_entry_type = mapping.en_move_type
        stock_entry.purpose = mapping.en_move_type
        stock_entry.reason_code = mapping.en_reason_code or None
        stock_entry.remarks = f"MES移动类型: {mes_move_type}, 原因: {mes_reason_code}"

        # 3. 遍历 items 列表，动态取字段
        for i, item in enumerate(items):
            item_code = item.get(mapping.item_field)
            source_wh = item.get(mapping.source_wh_field)
            target_wh = item.get(mapping.target_wh_field)
            qty = flt(item.get(mapping.qty_field) or 0)
            batch_no = item.get("batch_no")
            uom = item.get("uom")
            cost_center_field = mapping.cost_center_field
            
            # 校验必要字段
            if not item_code:
                frappe.throw(f"第 {i+1} 行缺少物料字段 ({mapping.item_field})")
            if not source_wh and not target_wh:
                frappe.throw(f"第 {i+1} 行未提供发料仓或收料仓字段")
            if qty <= 0:
                frappe.throw(f"第 {i+1} 行数量无效 ({mapping.qty_field})")

            # 批次号非必填
            has_batch_no=frappe.get_value(
                "Item",
                item_code,
                "has_batch_no"
            )
            #若开启了批次号管理，批次号做校验
            if has_batch_no == 1:
                if not batch_no:
                    frappe.throw(f"第 {i+1} 行缺少批次号 (batch_no)")

            if not uom:
                frappe.throw(f"第 {i+1} 行缺少计量单位 (uom)")

            # 组装明细行
            row_data = {
                "item_code": item_code,
                "qty": qty,
                "uom": item.get("uom") or "Nos",
                "batch_no": item.get("batch_no"),
                "use_serial_batch_fields": has_batch_no
            }
            # TODO 处理EN移动原因

            # 处理成本中心
            cost_center = None
            if cost_center_field:
                if cost_center_field.startswith("="):
                    # 去掉等号，拆成“仓库字段.字段名”
                    ref = cost_center_field[1:]  # 例如 "t_warehouse.custom_cost_center"
                    wh_field, cc_field = ref.split(".", 1)
                    # 根据行里的仓库字段获取对应仓库
                    wh_name = item.get(wh_field)
                    if wh_field == "s_warehouse":
                        wh_name = source_wh
                    elif wh_field == "t_warehouse":
                        wh_name = target_wh
                    if not wh_name:
                        frappe.throw(f"第 {i+1} 行缺少仓库字段 {wh_field}")
                    if wh_name:
                        cost_center = frappe.db.get_value("Warehouse", wh_name, cc_field)
                    if not cost_center:
                        frappe.throw(f"第 {i+1} 行仓库 {wh_name} 未维护字段 {cc_field}")
                else:
                    # 普通字段，直接从 items 取
                    cost_center = item.get(cost_center_field)
                    if not cost_center:
                        frappe.throw(f"第 {i+1} 行缺少字段 {cost_center_field} 的值")

            if source_wh:
                row_data["s_warehouse"] = source_wh
            if target_wh:
                row_data["t_warehouse"] = target_wh
            if cost_center:
                row_data["cost_center"] = cost_center
            
            # row_data["expense_account"]=""
            stock_entry.append("items", row_data)

        # EN移动原因逻辑
        first_item = items[0] if items else None
        if not first_item:
            frappe.throw("物料明细不能为空")

        # 如果配置了移动原因
        en_reason_code = mapping.en_reason_code
        if en_reason_code:
            if en_reason_code.startswith("="):
                # 例如 "=cost_center.custom_potion_attributes"
                ref = en_reason_code[1:]
                parts = ref.split(".", 1)
                if len(parts) != 2:
                    frappe.throw(f"移动原因配置格式错误: {en_reason_code}")
                ref_type, ref_field = parts
                ref_value = None

                if ref_type == "cost_center":
                    # 先根据第一行获取成本中心
                    cost_center_field = mapping.cost_center_field
                    cost_center = None
                    if cost_center_field:
                        if cost_center_field.startswith("="):
                            ref2 = cost_center_field[1:]
                            wh_field, cc_field = ref2.split(".", 1)
                            wh_name = first_item.get(mapping.source_wh_field) if wh_field == "s_warehouse" else first_item.get(mapping.target_wh_field)
                            cost_center = frappe.db.get_value("Warehouse", wh_name, cc_field)
                        else:
                            cost_center = first_item.get(cost_center_field)

                    if not cost_center:
                        frappe.throw("无法从第一行确定成本中心")

                    ref_value = frappe.db.get_value("Cost Center", cost_center, ref_field)
                    if not ref_value:
                        frappe.throw(f"成本中心 {cost_center} 未维护字段 {ref_field}")

                else:
                    frappe.throw(f"暂不支持引用类型: {ref_type}")

                # 判断映射规则
                if ref_value == "long-term":
                    stock_entry.reason_code = "MEDICINE_IN_LONG"
                    expense_account = frappe.db.get_value(
                    "Material Movement Default Account",
                    {"parent": "MEDICINE_IN_LONG"},
                    "expense_account"
                    )
                    stock_entry.expense_account = expense_account
                    for i in stock_entry.items:
                        i.expense_account = expense_account
                elif ref_value == "consume":
                    stock_entry.reason_code = "MEDICINE_IN_SHORT"
                else:
                    stock_entry.reason_code = ref_value
        #如果move_expense_account配置了=reason_code.expense_account，那么会根据reason_code那边获取到费用科目，费用科目是expense_account，这个字段
        #并填入物料表和物料明细表
        expense=mapping.get("move_expense_account",None)
        if expense:
            try:
                # expense 格式: reason_code.expense_account
                if "." not in expense and "=" not in expense:
                    return {
                        "status": "error",
                        "message": f"{expense} 格式不正确，应为 reason_code.xxx"
                    }
                expense=expense[1:]

                head, field = expense.split(".", 1)

                if head != "reason_code":
                    return {
                        "status": "error",
                        "message": f"{expense} 不支持的类型"
                    }

                # reason_code 必须存在
                if not stock_entry.reason_code:
                    return {
                        "status": "error",
                        "message": "未设置移动原因(reason_code)"
                    }

                # 获取 Material Movement Default Account 中对应字段
                expense_account = frappe.db.get_value(
                    "Material Movement Default Account",
                    {"parent": stock_entry.reason_code},
                    field
                )

                if not expense_account:
                    return {
                        "status": "error",
                        "message": f"移动原因 {stock_entry.reason_code} 未配置 {field} 的费用科目"
                    }

                # 写入主表和子项
                stock_entry.expense_account = expense_account
                for i in stock_entry.items:
                    i.expense_account = expense_account

            except Exception as e:
                frappe.logger().error(str(e))
                return {
                    "status": "error",
                    "message": "系统处理费用科目时发生异常，请检查配置"
                }


        # # 4. 插入 & 提交

        #物料移动先进先出规则
        if mes_move_type == 'INVENTORY_MOVE' and mes_reason_code == 'RAW_MATERIAL_OUT':
            flag,result=first_in_first_out(stock_entry)
            if not flag:
                frappe.local.response["http_status_code"] = 500
                return result
        stock_entry.insert(ignore_permissions=True)
        stock_entry.submit()

        frappe.db.commit()

        return {
            "status": "success",
            "stock_entry": stock_entry.name,
            "message": "物料移动单已创建成功"
        }
    except BatchNegativeStockError as e:
        # e.args[0] 通常是原始错误消息
        frappe.db.rollback()
        msg = e.args[0] if e.args else "批次库存不足"
        
        # 使用正则提取批次号、物料、仓库、数量
        m = re.search(
            r"Batch No <strong>(.*?)</strong> of an Item <strong>(.*?)</strong>.*?quantity <strong>(.*?)</strong>.*?warehouse (.*?)",
            msg, re.DOTALL
        )
        if m:
            batch_no, item_code, qty, warehouse = m.groups()
            message = f"物料 {item_code} 的批次 {batch_no} 在仓库 {warehouse} 库存不足，缺少数量 {qty}"
        else:
            message = "批次库存不足，请检查库存"
        frappe.local.response["http_status_code"] = 500
        frappe.local.response["content_type"] = "application/json; charset=utf-8"
        return {
            "status": "error",
            "stock_entry": None,
            "message": message
        }
    except Exception as e:
        # 记录到后台错误日志
        frappe.db.rollback()
        frappe.logger().error(frappe.get_traceback(), "MES Material Movement Error")
        # frappe.logger("mes_movement").error(error_msg)
        frappe.local.response["http_status_code"] = 500
        frappe.local.response["content_type"] = "application/json; charset=utf-8"
        return {
            "status": "error",
            "stock_entry": None,
            "message":str(e)  # 简化后的提示
        }

from erpnext.stock.doctype.batch.batch import get_batch_qty

def first_in_first_out(stock_entry):
    """
    先进先出判定函数
    """
    manage_store = ["HX02-002", "HX02-001", "HX01-001", "HX01-002", "HX01-004"]
    error_msg=None
    for item in stock_entry.items:
        if (
            item.s_warehouse in manage_store and
            item.batch_no and
            item.item_code
        ):
            batches = frappe.get_all(
                "Batch",
                filters={
                    "item": item.item_code,
                    "batch_qty": [">", 0],
                    "disabled": 0,
                },
                or_filters=[
                    ["expiry_date", ">=", frappe.utils.nowdate()],
                    ["expiry_date", "=", ""],
                    ["expiry_date", "is", "not set"],
                ],
                fields=["name", "manufacturing_date", "creation", "reference_name"],
                order_by="name asc"
            )
            if not batches:
                return(False,f"物料 {item.item_code} 没有可用的批号库存")
            earliest_batch = None
            # frappe.logger().error(f"{batches}")
            for batch in batches:
                s_warehouse = frappe.get_value(
                    "Purchase Receipt Item",
                    {
                        "parent": batch.reference_name,
                        "batch_no": batch.name
                    },
                    "warehouse"
                )
                if s_warehouse not in manage_store:
                    continue
                qty_by_wh = get_batch_qty(
                    batch_no=batch.name,
                    item_code=item.item_code,
                    for_stock_levels=0,
                    consider_negative_batches=1
                )
                if not qty_by_wh:
                    return(False,f"批号 {batch.name} 查询库存失败")
                for row in qty_by_wh:
                    wh = row.get("warehouse")
                    if wh != item.s_warehouse:
                        continue
                    qty = row.get("qty", 0)
                    if qty > 0:
                        earliest_batch = {
                            "name":batch.name,
                            "qty":qty,
                            "warehouse":wh
                        }
                        # frappe.logger().error(f"{batch},{s_warehouse},{qty_by_wh}")
                        break
                if earliest_batch is not None:
                    break

            if earliest_batch is None:
                return(False,f"物料 {item.item_code} 在仓库 {item.s_warehouse} 中查找更早的编号时出现问题!请看详情问题")
            if item.batch_no != earliest_batch['name']:
                frappe.logger().error(f"物料{item.item_code} 在使用批号{item.batch_no}时违反了先进先出的原则,请优先使用{earliest_batch['name']}")
                return (False,{
                    "status":"error",
                    "message":f"物料 {item.item_code} 在仓库 {earliest_batch['warehouse']} 中有更早的批号:{earliest_batch['name']}"
                    f"当前使用的是：{item.batch_no},请优先使用最早批号以遵循先进先出原则。"
                })
    return (True,None)